package com.nineclock.attendance.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nineclock.attendance.dto.AttendGroupDTO;
import com.nineclock.attendance.dto.AttendPunchDTO;
import com.nineclock.attendance.dto.AttendPunchUserWholeDayDTO;
import com.nineclock.attendance.dto.AttendsDTO;
import com.nineclock.attendance.enums.AttendEnums;
import com.nineclock.attendance.excel.CustomHandler;
import com.nineclock.attendance.mapper.AttendPunchMapper;
import com.nineclock.attendance.pojo.AttendPunch;
import com.nineclock.attendance.service.AttendGroupService;
import com.nineclock.attendance.service.AttendPunchService;
import com.nineclock.common.entity.Result;
import com.nineclock.common.enums.ResponseEnum;
import com.nineclock.common.exception.NcException;
import com.nineclock.common.threadlocal.CurrentUserHolder;
import com.nineclock.common.utils.BeanHelper;
import com.nineclock.common.utils.DateTimeUtil;
import com.nineclock.common.utils.DistanceUtil;
import com.nineclock.system.dto.SysCompanyUserDTO;
import com.nineclock.system.feign.SysUserFeign;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Transactional
public class AttendPunchServiceImpl implements AttendPunchService {

    @Autowired
    private AttendPunchMapper attendPunchMapper;
    @Autowired
    private AttendGroupService attendGroupService;
    @Autowired
    private SysUserFeign sysUserFeign;

    /**
     * 移动端打卡
     *
     * @param punchDTO
     */
    @Override
    public void punch(AttendPunchDTO punchDTO) {

        //1. 参数校验
        if (punchDTO == null) {
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }

        //2. 封装打卡信息
        AttendPunch attendPunch = BeanHelper.copyProperties(punchDTO, AttendPunch.class);

        Date now = new Date();
        attendPunch.setCompanyId(CurrentUserHolder.get().getCompanyId());
        attendPunch.setCompanyUserId(CurrentUserHolder.get().getCompanyUserId());
        attendPunch.setCreateTime(now);

        //2.1 设置打卡时间
        attendPunch.setPunchTime(now);
        attendPunch.setPunchDateStr(DateTimeUtil.dateToStr(now, DateTimeUtil.TIME_FORMAT_2));

        //2.2 判断上下午 ---> 数据库存储是 1:上午  2:下午
        int noonType = DateTimeUtil.noonType(now); //工具类中返回值 0上午 1下午
        attendPunch.setPunchOnOffWork(noonType + 1);

        //3. 获取到用户考勤组
        AttendGroupDTO attendGroup = attendGroupService.queryAttendGroup();
        //3.1 判定考勤组
        if (attendGroup == null) {
            throw new NcException(ResponseEnum.USER_NOT_MATCH_ATTENDGROUP);
        }

        //4. 判断是否为有效打卡
        //4.1 判断打卡是否在有效范围内 ---> true: 在范围之内 ; false: 超出范围
        boolean flag = this.validateArea(punchDTO, attendGroup);
        if (!flag) {
            throw new NcException(ResponseEnum.PUNCH_INVALID_AREA);
        }
        attendPunch.setAreaValid(true);

        //4.2 判断是否为工作日打卡 ----> 非工作日不用打卡
        boolean isWorkDay = this.validateWorkDay(attendGroup);
        if (!isWorkDay) {
            throw new NcException(ResponseEnum.PUNCH_INVALID_DAY);
        }
        attendPunch.setDayType(AttendEnums.DAY_TYPE_WORKDAY.value());

        //4.3 判断是否为有效打卡
        boolean effective = this.validateEffective(attendPunch);
        if (effective) {
            //4.3.1 判定有效打卡后,判断是 正常 迟到 早退 旷工...
            this.setPunchType(attendPunch, attendGroup);
        }

        //4.4. 保存打卡数据信息
        attendPunchMapper.insert(attendPunch);
    }

    //判断打卡 : 1正常；2迟到；3早退；4旷工 5非工作日打卡
    private void setPunchType(AttendPunch attendPunch, AttendGroupDTO attendGroupDTO) {
        //获取考勤组中的 允许迟到分钟数
        Integer allowLateMinutes = attendGroupDTO.getAllowLateMinutes();

        //获取考勤组中的 旷工迟到分钟数
        Integer lateMinutes = attendGroupDTO.getLateMinutes();

        //获取实际打卡时间字符串
        Date punchTime = attendPunch.getPunchTime();
        String punchTimeStr = DateTimeUtil.dateToStr(punchTime, DateTimeUtil.TIME_FORMAT_1);

        //默认正常
        AttendEnums punchType = AttendEnums.PUNCH_TYPE_OK;

        //判定
        //上午
        if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) {
            //获取关联的考勤组的设置上班时间
            String startWorkTime = attendGroupDTO.getStartWorkTime();

            //判定上班打卡时间
            if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, allowLateMinutes) == 1) { //返回1 , 说明 : punchTimeStr > startWorkTime + allowLateMinutes
                punchType = AttendEnums.PUNCH_TYPE_LATE;
                if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, lateMinutes) == 1) {  //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;
                }
            }
            attendPunch.setMorningPunchType(punchType.value());

            //下午
        } else {
            String offWorkTime = attendGroupDTO.getOffWorkTime();

            //判定下班打卡时间
            if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, allowLateMinutes) == 1) { //返回1 , 说明 : offWorkTime > punchTimeStr + allowLateMinutes
                punchType = AttendEnums.PUNCH_TYPE_EARLY;
                if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, lateMinutes) == 1) {  //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;
                }
            }
            attendPunch.setAfternoonPunchType(punchType.value());
        }
    }

    //判断上下午打卡是否有效
    private boolean validateEffective(AttendPunch attendPunch) {

        //1. 查询当前用户的今日 上午/下午 是否打过卡
        LambdaQueryWrapper<AttendPunch> punchWrapper = new LambdaQueryWrapper<>();
        punchWrapper
                .eq(AttendPunch::getCompanyId, CurrentUserHolder.get().getCompanyId())
                .eq(AttendPunch::getCompanyUserId, CurrentUserHolder.get().getCompanyUserId()) //打卡用户ID
                .eq(AttendPunch::getPunchDateStr, attendPunch.getPunchDateStr()) //打卡时间
                .eq(AttendPunch::getPunchOnOffWork, attendPunch.getPunchOnOffWork()) //上午/下午
                .eq(AttendPunch::getAreaValid, true) //打卡范围有效(可以不加)
                .eq(AttendPunch::getDayType, 1); //工作日打卡(可以不加)

        Integer count = attendPunchMapper.selectCount(punchWrapper);

        //2. 如果未获取到, 说明没有打过卡, 此次打卡, 无论是上午还是下午都是有效的
        if (count == 0) {
            attendPunch.setEffectiveValid(true); //有效

        } else {
            //3. 如果获取到记录, 说明已经打过卡
            //3.1 如果是上午, 第一条有效, 其余都是无效的
            if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) {
                attendPunch.setEffectiveValid(false);

            } else {
                //3.2 如果是下午, 最后一条有效,  其余都是无效的
                attendPunch.setEffectiveValid(true);

                //更新之前的打卡记录为无效, 以最后一次打卡为准
                AttendPunch punch = new AttendPunch();
                punch.setEffectiveValid(false);
                attendPunchMapper.update(punch, punchWrapper);
            }
        }

        return attendPunch.getEffectiveValid();
    }

    //校验工作日
    private boolean validateWorkDay(AttendGroupDTO attendGroupDTO) {
        int week = DateUtil.thisDayOfWeek();   //获取当前是星期几  1: 表示周日, 2: 表示周一, 3:表示周二

        List<String> workdays = attendGroupDTO.getWorkdays(); // [1,1,1,1,1,0,0]

        int index = 0;

        if (week == 1) {
            index = 6;
        } else {
            index = week - 2;
        }

        boolean flag = Integer.valueOf(workdays.get(index)) == 1;
        return flag;
    }

    //校验打卡范围
    private boolean validateArea(AttendPunchDTO attendPunchDTO, AttendGroupDTO attendGroupDTO) {
        //比较"打卡坐标" 和 "考勤组打卡坐标"的距离
        Integer meter =
                DistanceUtil.getDistanceMeter(
                        attendPunchDTO.getLat().doubleValue(),
                        attendPunchDTO.getLng().doubleValue(),
                        attendGroupDTO.getLat().doubleValue(),
                        attendGroupDTO.getLng().doubleValue()
                );

        //判定是否小于等于考勤组中设置的范围
        return meter <= attendGroupDTO.getAddressRange();
    }

/***************************************************************************************************************/

    /**
     * 根据系统用户ID获取打卡数据
     *
     * @return
     */
    @Override
    public AttendPunchUserWholeDayDTO getPunchRecord() {

        //获取企业员工信息,当前时间
        Long companyUserId = CurrentUserHolder.get().getCompanyUserId();
        Long companyId = CurrentUserHolder.get().getCompanyId();
        String nowStr = DateTimeUtil.dateToStr(new Date(), DateTimeUtil.TIME_FORMAT_2);

        //1. 查询上午有效打卡情况
        LambdaQueryWrapper<AttendPunch> amWrapper = new LambdaQueryWrapper<>();
        amWrapper
                .eq(AttendPunch::getCompanyId, companyId)
                .eq(AttendPunch::getCompanyUserId, companyUserId)
                .eq(AttendPunch::getPunchDateStr, nowStr)
                .eq(AttendPunch::getPunchOnOffWork, AttendEnums.NOON_TYPE_MORNING.value())
                .eq(AttendPunch::getEffectiveValid, true);

        AttendPunch amPunch = attendPunchMapper.selectOne(amWrapper);

        //2. 查询下午有效打卡情况
        LambdaQueryWrapper<AttendPunch> pmWrapper = new LambdaQueryWrapper<>();
        pmWrapper.eq(AttendPunch::getCompanyId, companyId)
                .eq(AttendPunch::getCompanyUserId, companyUserId)
                .eq(AttendPunch::getEffectiveValid, true)
                .eq(AttendPunch::getPunchOnOffWork, AttendEnums.NOON_TYPE_AFTERNOON.value())
                .eq(AttendPunch::getPunchDateStr, nowStr);

        AttendPunch pmPunch = attendPunchMapper.selectOne(pmWrapper);

        //3. 组装数据 返回
        AttendPunchUserWholeDayDTO wholeDayDTO = new AttendPunchUserWholeDayDTO();

        if (amPunch != null) {
            AttendPunchDTO amPunchDto = BeanHelper.copyProperties(amPunch, AttendPunchDTO.class);
            wholeDayDTO.setAttendPunchMorningResponse(amPunchDto);
        }
        if (pmPunch != null) {
            AttendPunchDTO pmPunchDto = BeanHelper.copyProperties(pmPunch, AttendPunchDTO.class);
            wholeDayDTO.setAttendPunchAfterNoonResponse(pmPunchDto);
        }
        return wholeDayDTO;
    }

/***************************************************************************************************************/

    /**
     * 查询指定时间段内的考勤列表数据
     * 所有员工 + 指定时间段 + 时间段内有效打卡数据
     *
     * @param startTime
     * @param endTime
     * @return
     */
    @Override
    public List<AttendPunchDTO> queryMembers(String startTime, String endTime) {
        //参数校验
        if (StrUtil.isEmpty(startTime) || StrUtil.isEmpty(endTime)) {
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }

        //1. 获取两个日期时间范围之间的日期列表
        List<Date> dateList = DateTimeUtil.getDatesBetweenTwoDate(startTime, endTime, DateTimeUtil.TIME_FORMAT_2);
        List<String> dateStrList = dateList.stream().map(date -> {
            return DateTimeUtil.dateToStr(date, DateTimeUtil.TIME_FORMAT_2);
        }).collect(Collectors.toList());

        //2. 获取当前企业下所有的用户数据
        Result<List<SysCompanyUserDTO>> result = sysUserFeign.queryAllCompanyUser();
        List<SysCompanyUserDTO> companyUserDTOs = result.getData();

        //3. 遍历员工集合 查询每个员工每天的打卡情况 ↓↓↓
        List<AttendPunchDTO> attendPunchDTOList = getAttendPunchDTOS(startTime, endTime, dateStrList, companyUserDTOs);

        //4. 组装数据返回
        return attendPunchDTOList;
    }

    //获取指定日期范围内, 指定员工的考勤打卡情况
    private List<AttendPunchDTO> getAttendPunchDTOS(String startTime, String endTime,
                                                    List<String> dateStrList,
                                                    List<SysCompanyUserDTO> companyUserDTOs) {

        List<AttendPunchDTO> punchDTOS = companyUserDTOs.stream().map(sysCompanyUserDTO -> {

            //查询员工的打卡情况 --> 员工ID 时间范围
            LambdaQueryWrapper<AttendPunch> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(AttendPunch::getCompanyUserId, sysCompanyUserDTO.getId()); //员工ID
            wrapper.eq(AttendPunch::getEffectiveValid, true);//有效
            wrapper.between(AttendPunch::getPunchDateStr, startTime, endTime); //时间范围

            List<AttendPunch> attendPunches = attendPunchMapper.selectList(wrapper);

            //封装当前员工的打卡数据
            AttendPunchDTO attendPunchDTO = BeanHelper.copyProperties(sysCompanyUserDTO, AttendPunchDTO.class);

            //查询员工每一天的打卡情况 ↓↓↓
            List<AttendsDTO> attendsList = this.handleAttendPunchEveryDay(dateStrList, attendPunches);
            attendPunchDTO.setAttendsList(attendsList);

            return attendPunchDTO;
        }).collect(Collectors.toList());

        return punchDTOS;
    }

    //查询员工每一天的打卡情况
    private List<AttendsDTO> handleAttendPunchEveryDay(List<String> dateStrList, List<AttendPunch> attendPunches) {
        //将打卡信息列表进行 分组处理
        //key 2021-04-01 : value List<AttendPunch>
        Map<String, List<AttendPunch>> punchMap = attendPunches.stream().collect(Collectors.groupingBy(AttendPunch::getPunchDateStr));

        //获取每一天的打卡情况
        List<AttendsDTO> attendsDTOS = dateStrList.stream().map(dateStr -> {

            AttendsDTO attendsDTO = new AttendsDTO();
            attendsDTO.setAttendDate(dateStr);

            List<AttendPunch> punchList = punchMap.get(dateStr); //获取到某一天的打卡数据

            //如果为空, 则说明上下午都没有打卡
            if (CollectionUtil.isEmpty(punchList)) {
                attendsDTO.setAttendStatus("缺卡 / 缺卡");
                //如果不是全天"缺卡 / 缺卡" ↓↓↓
            } else {
                attendsDTO.setAttendStatus(this.handleAttendStatus(punchList));
            }
            return attendsDTO;
        }).collect(Collectors.toList());

        return attendsDTOS;
    }

    //如果不是全天 "缺卡 / 缺卡"
    private String handleAttendStatus(List<AttendPunch> punchList) {
        String amAttendStatus = "缺卡";   //上午 - 考勤
        String pmAttendStatus = "缺卡";   //下午 - 考勤

        //遍历某一天的打卡数据
        for (AttendPunch punch : punchList) {
            if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) { //上午打卡
                amAttendStatus = this.getPushDesc(punch.getMorningPunchType());

            } else if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_AFTERNOON.value()) { //下午打卡
                pmAttendStatus = this.getPushDesc(punch.getAfternoonPunchType());
            }
        }
        return amAttendStatus + " / " + pmAttendStatus;
    }

    //打卡类型
    private String getPushDesc(Integer morningPunchType) {
        /*
        PUNCH_TYPE_OK(1, "正常"),
        PUNCH_TYPE_LATE(2, "迟到"),
        PUNCH_TYPE_EARLY(3, "早退"),
        PUNCH_TYPE_STAYALAY(4, "旷工"),
        PUNCH_TYPE_NOT_WORK_DAY(5, "非工作日打卡"),
         */
        String desc = "";
        switch (morningPunchType) {
            case 1:
                desc = AttendEnums.PUNCH_TYPE_OK.toString();
                break;
            case 2:
                desc = AttendEnums.PUNCH_TYPE_LATE.toString();
                break;
            case 3:
                desc = AttendEnums.PUNCH_TYPE_EARLY.toString();
                break;
            case 4:
                desc = AttendEnums.PUNCH_TYPE_STAYALAY.toString();
                break;
            case 5:
                desc = AttendEnums.PUNCH_TYPE_NOT_WORK_DAY.toString();
                break;
            default:
                break;
        }
        return desc;
    }

/***************************************************************************************************************/

    /**
     * 导出考勤数据
     *
     * @param startTime
     * @param endTime
     */
    @Override
    public void export(String startTime, String endTime) throws IOException {

        //1. 获取response对象,设置文件下载的响应头信息
        /* 任意地点获取request response ---> RequestContextHolder.getRequestAttributes() */
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = requestAttributes.getResponse();

        response.setHeader("Content-disposition", "attachment;filename=Attendance.xls");
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setCharacterEncoding("utf-8");

        //2. 查询指定时间内的考勤打卡数据
        List<AttendPunchDTO> punchDTOList = this.queryMembers(startTime, endTime); //↑↑↑

        //3. 通过EasyExcel的API生成文件, 使用流的形式输出
        EasyExcel.write(response.getOutputStream())
                .excelType(ExcelTypeEnum.XLS)
                .head(headers(punchDTOList)) //组装表头
                .sheet("考勤记录")
                .registerWriteHandler(new CustomHandler()) //设置列宽
                .registerWriteHandler(horizontalCellStyleStrategy()) //设置字体 居中
                .doWrite(handleData(punchDTOList)); //组装表数据
    }

    //组装表头信息
    private List<List<String>> headers(List<AttendPunchDTO> punchDTOList) {
        List<List<String>> headers = new ArrayList<>();

        headers.add(Arrays.asList("姓名"));
        headers.add(Arrays.asList("工号"));
        headers.add(Arrays.asList("部门"));
        headers.add(Arrays.asList("职位"));

        //动态表头
        //拿到punchDTOList列表里的 一个数据(一个打卡记录列表)
        if (CollectionUtil.isNotEmpty(punchDTOList)) {
            AttendPunchDTO attendPunchDTO = punchDTOList.get(0);
            List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();

            if (attendsList != null) {
                //获取AttendDate
                for (AttendsDTO attendsDTO : attendsList) {
                    headers.add(Arrays.asList(attendsDTO.getAttendDate()));
                }
            }
        }
        return headers;
    }

    //组装行数据
    private List<List<String>> handleData(List<AttendPunchDTO> punchDTOList) {
        List<List<String>> dataList = new ArrayList<>();

        if (CollectionUtil.isNotEmpty(punchDTOList)) {
            for (AttendPunchDTO punchDTO : punchDTOList) {
                List<String> rowList = new ArrayList<>();

                //姓名, 工号, 部门, 职位
                rowList.add(punchDTO.getUserName());
                rowList.add(punchDTO.getWorkNumber());
                rowList.add(punchDTO.getDepartmentName());
                rowList.add(punchDTO.getPost());

                //动态信息: 每日打卡情况
                List<AttendsDTO> attendsList = punchDTO.getAttendsList();
                if (CollectionUtil.isNotEmpty(attendsList)) {
                    for (AttendsDTO dto : attendsList) {
                        rowList.add(dto.getAttendStatus());
                    }
                }
                dataList.add(rowList);
            }
        }
        return dataList;
    }

    //设置样式
    public HorizontalCellStyleStrategy horizontalCellStyleStrategy() {

        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //内容策略 - 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

        //设置字体
        WriteFont font = new WriteFont();
        font.setFontName("阿里巴巴普惠体");
        font.setFontHeightInPoints((short) 11);
        contentWriteCellStyle.setWriteFont(font);

        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(contentWriteCellStyle, contentWriteCellStyle);

        return horizontalCellStyleStrategy;
    }
    /***************************************************************************************************************/

    /**
     * 根据企业ID查询该企业本月考勤信息, 并生成Excel文件
     *
     * @param companyId
     * @return
     */
    @Override
    public String generateExcelReportByCompanyId(Long companyId) {

        //1. 获取本月的第一天及最后一天, 获取日期列表
        String firstDay = DateTimeUtil.getCurrentMonthFirstDay();
        String lastDay = DateTimeUtil.getCurrentMonthLastDay();

        List<Date> dateList = DateTimeUtil.getDatesBetweenTwoDate(firstDay, lastDay, DateTimeUtil.TIME_FORMAT_2);
        List<String> dateStrList = dateList.stream().map(date -> {
            return DateTimeUtil.dateToStr(date, DateTimeUtil.TIME_FORMAT_2);
        }).collect(Collectors.toList());

        //2. 获取企业ID对应的员工列表
        List<SysCompanyUserDTO> companyUserDTOS = sysUserFeign.queryCompanyUser(null, companyId).getData();

        //3. 获取该企业员工打卡数据
        List<AttendPunchDTO> punchDTOS = this.getAttendPunchDTOS(firstDay, lastDay, dateStrList, companyUserDTOS);

        //4. 生成Excel
        String folderName = "E:\\考勤数据\\" + companyId;
        String filename = folderName + "\\考勤报表-" + firstDay.substring(0, 7) + ".xls"; //2021-04

        if (!new File(folderName).exists()) {
            new File(folderName).mkdirs();
        }

        EasyExcel.write(new File(filename))
                .excelType(ExcelTypeEnum.XLS)

                .registerWriteHandler(new CustomHandler()) //列宽
                .registerWriteHandler(horizontalCellStyleStrategy()) //字体样式

                .sheet("考勤数据")
                .head(headers(punchDTOS)) //表头
                .doWrite(handleData(punchDTOS)); //表数据

        return filename;
    }

}    